<?php
/**
 * @file
 * Contains HumbleDatabase.
 *
 * @author  Jacob Flygenring <jf@jayscape.net>
 * @license GNU GPL v3 (http://www.gnu.org/licenses/gpl.html)
 */

/**
 * Provides some shortcut functions to performing database actions and provides some level of error handling.
 *
 * @version    Release: 1.3.3
 * @link       http://code.google.com/p/humble-database/
 */
if(!class_exists('HumbleDatabase')) {
    class HumbleDatabase extends PDO {

        private $_arrBind;
        private $_strError;
        private $_strQuery;
        private $_strBoundQuery;
        private $_objPDOStatement;
        private $_boolDebug = false;
        private $_mixDebugFunction; // = array($this, 'pprint_r');

        /**
         * Constructs a HumbleDatabase object by setting up the database connection in a somewhat intelligent way.
         *
         * @param string $strUser
         *   Username for the database.
         * @param string $strPass
         *   Password for the database.
         * @param string $strDatabaseName
         *   Name of the database to connect to.
         * @param string $strServer
         *   (optional) Address for the database server. Defaults to 'localhost'.
         * @param int $intPort
         *   (optional) Port on the server to connect to. Defaults to 3306.
         * @param string $strDriver
         *   (optional) Database driver to use for the connection. Defaults to 'mysql'.
         * @param string $strCharset
         *   (optional) Character set to use for the connection. Defaults to 'utf8'.
         */
        public function __construct($strUser, $strPass, $strDatabaseName, $strServer = 'localhost',
                                    $intPort = 3306, $strDriver = 'mysql', $strCharset = 'utf8') {
            $arrOptions = array(
                PDO::ATTR_PERSISTENT => true,
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
            );
            $strDatabaseSetup = $strDriver . ':'
                              . 'host=' . $strServer . ';'
                              . 'dbname=' . trim($strDatabaseName, '`´\'"') . ';'
                              . 'port=' . $intPort . ';'
                              . 'charset=' . $strCharset;
            try {
                parent::__construct($strDatabaseSetup, $strUser, $strPass, $arrOptions);
            } catch(PDOException $objError) {
                $this->handleException($objError);
            }
            $this->setDebugFunction('pprint_r');
        }

        /**
         * Cleans up used objects before the class is destroyed (unset).
         */
        public function __destruct() {
            $this->_objPDOStatement = null;
        }

        /**
         * Sets the debugging flag, activating or deactivating the debugger.
         *
         * @param bool $boolState
         *   The flag saying whether the debugger is activated or not.
         */
        public function setDebugging($boolState) {
            $this->_boolDebug = $boolState;
        }

        /**
         * Sets the function for handling the information passed from debug().
         *
         * @param string $strFunctionName
         *   The name of the debugging function to be used.
         *
         * @return boolean
         *   Returns true if $strFunctionName is a valid and callable function, otherwise false.
         */
        public function setDebugFunction($strFunctionName) {
            if(function_exists($strFunctionName) && is_callable($strFunctionName)) {
                $this->_mixDebugFunction = $strFunctionName;
                return true;
            } else if(method_exists($this, $strFunctionName)) {
                $this->_mixDebugFunction = array($this, $strFunctionName);
                return true;
            } else {
                return false;
            }
        }

        /**
         * Default function for the custom debugging function.
         *
         * @param mixed $mixVariable
         *   The variable to be output.
         * @param boolean $boolReturn
         *   (optional) Whether the output should be returned or output directly. Defaults to false.
         *
         * @return string
         *   Returns the output string if $boolReturn is true.
         */
        public function pprint_r($mixVariable, $boolReturn = false) {
            if($boolReturn === true) {
                return '<pre>' . print_r($mixVariable, true) . '</pre>';
            } else {
                echo '<pre>' . print_r($mixVariable, true) . '</pre>';
            }
        }

        /**
         * Handles exceptions caught in the other functions
         *
         * @param PDOException $objError
         *   The exception that was caught.
         *
         * @return mixed
         *   The return value from the debug() function, which return the return value of the custom debugging function.
         * @return boolean
         *   If there was no PDOException the function returns false.
         */
        public function handleException($objError) {
            if(is_object($objError) && 'PDOException' == get_class($objError)) {
                $this->_strError = $objError->getMessage();
                return $this->_debug();
            }
            return false;
        }

        /**
         * Collects debug and error information and puts them in an associative array which is passed to the custom debugging function.
         *
         * The array that is passed on to the custom debugging function contains (if they're set):
         *   - error: (optional) The message from the exception.
         *   - query: (optional) The query of the prepared SQL statement.
         *   - bind: (optional) The array of values bound to the SQL statement.
         *   - trace: (optional) An array containing the stacktrace of the call
         *     that ended with a call to this function, but if there is an
         *     exception in the stacktrace it stops after its stacktrace.
         *
         * @return mixed
         *   The return value from the custom debugging function.
         * @return boolean
         *   If debugging is disabled the function returns false.
         */
        private function _debug() {
            if($this->_boolDebug) {
                $arrDebug = array();
                if(!empty($this->_strError)) {
                    $arrDebug['error'] = $this->_strError;
                }
                if(!empty($this->_strQuery)) {
                    $arrDebug['query'] = $this->_strQuery;
                }
                if(!empty($this->_strBoundQuery)) {
                    $arrDebug['bound_query'] = $this->_strBoundQuery;
                }
                if(!empty($this->_arrBind)) {
                    $arrDebug['bind'] = $this->_arrBind;
                }

                $arrBacktrace = debug_backtrace();
                if(!empty($arrBacktrace)) {
                    $arrDebug['trace'] = array();
                    foreach($arrBacktrace as $arrInfo) {
                        if(isset($arrInfo['args'][0]) && is_object($arrInfo['args'][0]) && 'PDOException' == get_class($arrInfo['args'][0])) {
                            $arrDebug['trace'] = array_merge($arrDebug['trace'], $arrInfo['args'][0]->getTrace());
                            break;
                        } else {
                            $arrDebug['trace'][] = $arrInfo;
                        }
                    }
                }
                return call_user_func($this->_mixDebugFunction, $arrDebug);
            }
            return false;
        }

        /**
         * Returns the current SQL query without values bound to it.
         *
         * @return string
         *   The internal SQL query.
         */
        public function getQuery() {
            return $this->_strQuery;
        }

        /**
         * Returns the current SQL query including values that have been bound to it.
         *
         * @return string
         *   The internal bound SQL query.
         */
        public function getBoundQuery() {
            return $this->_strBoundQuery;
        }

        /**
         * Prepares the SQL statenent.
         *
         * @param string $strQuery
         *   The query to be prepared.
         *
         * @return PDOStatement
         *   The prepared statement.
         * @return boolean
         *   If preparetion of the SQL statement throws an exception false is returned.
         */
        public function prepare($strQuery) {
            try {
                $this->_strQuery = trim($strQuery);
                $this->_strBoundQuery = $this->_strQuery;
                $this->_objPDOStatement = parent::prepare($this->_strQuery);
                return $this->_objPDOStatement;
            } catch(PDOException $objError) {
                $this->handleException($objError);
            }
            return false;
        }

        /**
         * Binds values to the SQL statement and substitutes 'name' for 'value in the internal bound SQL query.
         *
         * @param array $arrBind
         *   An array of name-value or name-(value-type) pairs to bind, e.g.
         *   array('name', 'value') or array('name', array('value', PDO::PARAM_INT)).
         *
         * @return boolean
         *   If there $arrBind is an array and no exceptions are thrown the function returns true,
         *   otherwise false.
         */
        public function bind($arrBind) {
            $boolReturn = true;
            if(is_array($arrBind)) {
                $this->_arrBind = $arrBind;
                foreach($this->_arrBind as $strName => $strValue) {
                    if($strName[0] != ':') {
                        $strName = ':' . $strName;
                    }
                    if(is_array($strValue)) {
                        $intType = intval($strValue[1]);
                        $strValue = $strValue[0];
                    } else if(is_string($strValue)) {
                        $intType = PDO::PARAM_STR;
                    } else if(is_int($strValue)) {
                        $intType = PDO::PARAM_INT;
                    }
                    if($intType == PDO::PARAM_STR) {
                        $this->_strBoundQuery = str_replace($strName, "'" . $strValue . "'", $this->_strBoundQuery);
                    } else if($intType == PDO::PARAM_INT) {
                        $this->_strBoundQuery = str_replace($strName, $strValue, $this->_strBoundQuery);
                    }
                    try {
                        // bindParam binds variables as references, and thus can't be used for this
                        //return $this->_objPDOStatement->bindParam($strName, $strValue, $intType)
                        if(!($this->_objPDOStatement->bindValue($strName, $strValue, $intType))) {
                            $boolReturn = false;
                        }
                    } catch(PDOException $objError) {
                        $boolReturn = false;
                        $this->handleException($objError);
                    }
                }
                return $boolReturn;
            }
            return false;
        }

        /**
         * Executes the SQL statement.
         *
         * @return boolean
         *   If no exceptions are thrown the return value from the parent function is returned, otherwise false.
         */
        public function execute() {
            try {
                return $this->_objPDOStatement->execute();
            } catch(PDOException $objError) {
                $this->handleException($objError);
            }
            return false;
        }

        /**
         * Fetches a single row from the results returned by the execution of the SQL statement.
         *
         * @param int $intStyle
         *   (optional) The fetch style used for getting the row, defaults to PDO::FETCH_ASSOC.
         *
         * @return array
         *   The row returned from the parent function using type $intStyle.
         * @return boolean
         *   If no exceptions are thrown the return value from the parent function is returned, otherwise false.
         */
        public function fetch($intStyle = PDO::FETCH_ASSOC) {
            try {
                return $this->_objPDOStatement->fetch($intStyle);
            } catch(PDOException $objError) {
                $this->handleException($objError);
            }
            return false;
        }

        /**
         * Fetches all rows from the results returned by the execution of the SQL statement.
         *
         * @param int $intStyle
         *   (optional) The fetch style used for getting the row, defaults to PDO::FETCH_ASSOC.
         *
         * @return array
         *   Array of rows (also arrays) returned from the parent function using type $intStyle.
         * @return boolean
         *   If no exceptions are thrown the return value from the parent function is returned, otherwise false.
         */
        public function fetchAll($intStyle = PDO::FETCH_ASSOC) {
            try {
                return $this->_objPDOStatement->fetchAll($intStyle);
            } catch(PDOException $objError) {
                $this->handleException($objError);
            }
            return false;
        }

        /**
         * Returns the number of rows selected or affected by the execution of the statement.
         *
         * @return int
         *   The number of rows selected or affected by the execution of the statement.
         */
        public function rowCount() {
            return $this->_objPDOStatement->rowCount();
        }

        /**
         * Prepares, binds and executes a statement and return all rows or number of affected rows
         *
         * @param string $strQuery
         *   The query to be prepared.
         * @param array $arrBind
         *   (optional) An array of name-value or name-(value-type) pairs to bind, e.g.
         *   array('name', 'value') or array('name', array('value', PDO::PARAM_INT)), defaults to an empty array.
         * @param int $intStyle
         *   (optional) The fetch style used for getting the row, defaults to PDO::FETCH_ASSOC.
         *
         * @return array
         *   Array of rows (also arrays) returned from the parent function using type $intStyle.
         * @return int
         *   The number of rows selected or affected by the execution of the statement.
         * @return boolean
         *   If no exceptions are thrown the return value from the parent function is returned, otherwise false.
         */
        public function run($strQuery, $arrBind = array(), $intStyle = PDO::FETCH_ASSOC) {
            try {
                $this->prepare($strQuery);

                if(!empty($arrBind) && is_array($arrBind)) {
                    $this->_arrBind = $arrBind;
                    $this->bind($this->_arrBind);
                }

                if($this->execute() !== false) {
                    if(preg_match('/^(select|describe|pragma) /i', $this->_strQuery)) {
                        return $this->fetchAll($intStyle);
                    } else if(preg_match('/^(delete|insert|update) /i', $this->_strQuery)) {
                        return $this->rowCount();
                    }
                }
            } catch(PDOException $objError) {
                $this->handleException($objError);
            }
            return false;
        }

    }
}